package com.pokeoffice.server;

import java.util.List;
import java.util.Vector;

import com.pokeoffice.interfaces.Command;
import com.pokeoffice.interfaces.Queue;
import com.pokeoffice.interfaces.Worker;
import com.pokeoffice.logs.Log;

/**
 * The Class CommandQueue.
 *
 * @author Kyle Davis
 * @version 1.02
 * 
 * (Pattern: Thread Pool) This class uses the Thread Pool pattern,
 * which is used to optimize the application by reducing the number of
 * Threads that need to be created. Instead it uses a list of threads
 * that all pull from the same queue of commands
 */
public class CommandQueue implements Queue {

	/** Default value for maxCommandsPerThread. */
	public static final int DEFAULT_COMMANDS_PER_THREAD = 25;

	/** Default value for maxIdleThreads. */
	public static final int DEFAULT_IDLE_THREADS = 5;

	/** Default value for maxThreads. */
	public static final int DEFAULT_THREADS = 100;

	/** The commands. */
	private final List<Command> commands;

	/** The debug. */
	private volatile boolean debug;

	/** The empty. */
	private volatile boolean empty;

	/** The extra workers. */
	private volatile boolean extraWorkers;

	/** The max commands per thread. */
	private volatile int maxCommandsPerThread;

	/** The max idle threads. */
	private volatile int maxIdleThreads;

	/** The max threads. */
	private volatile int maxThreads;

	/** The workers. */
	private final List<CommandWorker> workers;

	/**
	 * Default Constructor: Initializes lists, and variables. Starts the first
	 * Worker
	 */
	public CommandQueue() {
		Log.write(Log.SERV, this + "Initializing");

		commands = new Vector<Command>();
		debug = false;
		empty = true;
		extraWorkers = false;
		workers = new Vector<CommandWorker>();

		maxCommandsPerThread = DEFAULT_COMMANDS_PER_THREAD;
		maxIdleThreads = DEFAULT_IDLE_THREADS;
		maxThreads = DEFAULT_THREADS;

		Log.write(Log.SERV, this + "Creating first Worker");
		final CommandWorker work = new CommandWorker("Worker "
				+ (commands.size() + 1) + ": ");

		work.start();
		workers.add(work);
	}

	/**
	 * Places a command on the end of the CommandQueue.
	 *
	 * @param com a command that needs to be executed
	 */
	public synchronized void put(Command com) {
		if (debug) {
			Log.write(Log.SERV, "CommandQueue: " + "Adding command to Queue");
		}

		commands.add(com);

		if (commands.size() > maxCommandsPerThread * workers.size()) {
			if (workers.size() < maxThreads) {
				Log.write(Log.SERV, this + "Starting new Worker");
				final CommandWorker work = new CommandWorker("Worker "
						+ (workers.size() + 1) + ": ");
				work.start();
				workers.add(work);
			} else {
				Log.write(Log.SERV, this + "Maximum number of threads reached");
			}
		}

		if (empty) {
			empty = false;
			notifyAll();
		}

	}

	/**
	 * Sets the debug.
	 *
	 * @param debug the new debug
	 */
	public void setDebug(boolean debug) {
		this.debug = debug;
	}

	/**
	 * Sets the max commands per thread.
	 *
	 * @param max the new max commands per thread
	 * @throws IndexOutOfBoundsException the index out of bounds exception
	 */
	public void setMaxCommandsPerThread(int max)
			throws IndexOutOfBoundsException {
		if (max > 0) {
			maxCommandsPerThread = max;
		} else {
			throw new IndexOutOfBoundsException(max + " is not positive!");
		}
	}

	/**
	 * Sets the max idle threads.
	 *
	 * @param max the new max idle threads
	 * @throws IndexOutOfBoundsException the index out of bounds exception
	 */
	public void setMaxIdleThreads(int max) throws IndexOutOfBoundsException {
		if (max >= 0) {
			maxIdleThreads = max;
		} else {
			throw new IndexOutOfBoundsException(max + " is negative!");
		}
	}

	/**
	 * Sets the max threads.
	 *
	 * @param max the new max threads
	 * @throws IndexOutOfBoundsException the index out of bounds exception
	 */
	public void setMaxThreads(int max) throws IndexOutOfBoundsException {
		if (max > 0) {
			maxThreads = max;
		} else {
			throw new IndexOutOfBoundsException(max + " is not positive!");
		}
	}

	/**
	 * Shuts down all currently running threads FIXME Needs to be tested.
	 */
	public synchronized void shutdown() {
		Log.write(Log.SERV, this + "Shutting Down");

		for (CommandWorker work : workers) {
			work.shutdown();
		}

		workers.clear();
		notifyAll();

		Log.write(Log.SERV, this + "Successfully Shut Down");
	}

	/**
	 * Takes a command from the CommandQueue that needs to be executed.
	 *
	 * @return a command that needs to be executed
	 */
	public synchronized Command take() {
		while (empty) {
			try {
				wait();
			} catch (InterruptedException e) {
				if (debug) {
					Log.write(Log.SERV, this + "Interrupted in take()");
				}

				if (extraWorkers) {
					System.out.println("Shutdown a worker");
					extraWorkers = false;
					return null;
				}
			}
		}

		if (debug) {
			Log.write(Log.SERV, this + "Removing Command");
		}

		final Command com = commands.remove(0);

		if (commands.size() < ((workers.size() - maxIdleThreads))) {
			Log.write(Log.SERV, this + "Stopping Worker");
			workers.remove(workers.size() - 1).shutdown();
			extraWorkers = true;
			notifyAll();
		}

		if (commands.isEmpty()) {
			empty = true;
		}

		return com;
	}

	/**
	 * A string representation of CommandQueue.
	 *
	 * @return "CommandQueue"
	 */
	public String toString() {
		return "CommandQueue: ";
	}

	/**
	 * The Class CommandWorker.
	 *
	 * @author Kyle Davis
	 * 
	 * Thread subclass that runs commands
	 */
	private class CommandWorker extends Thread implements Worker {

		/** A flag that determines if the worker should stop. */
		private volatile boolean shutdown;

		/**
		 * Constructor that initializes shutdown to false.
		 *
		 * @param name The name of the worker
		 */
		private CommandWorker(String name) {
			super(name);
			shutdown = false;
		}

		/**
		 * Takes commands from the CommandQueue and executes them while this
		 * worker is not shutdown.
		 */
		public void run() {
			Log.write(Log.SERV, this + "Starting");

			while (!shutdown) {
				Command c = take();

				if (c != null) {
					if (debug) {
						Log.write(Log.SERV, this + "Executing Command");
					}

					c.execute();
				}
			}

			Log.write(Log.SERV, this + "Shutting Down");
		}

		/**
		 * Shutdown this Worker.
		 */
		public void shutdown() {
			this.shutdown = true;
			Log.write(Log.SERV, this + "should be shutting down soon");
		}

		/**
		 * A string representation of Worker.
		 *
		 * @return name of the worker
		 */
		public String toString() {
			return this.getName();
		}
	}
}